﻿using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using System;
using System.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace HiCore.Extensions.UnitOfWork.EntityFramework
{
    public static class SqlExtentions
    {
        public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
        {
            var command = QueryCommandSql(query);
            string sql = command.Command.CommandText;
            return sql;
        }

        public static (IRelationalCommand Command, IReadOnlyDictionary<string, object> Parameters) QueryCommandSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
        {
            var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
            var relationalCommandCache = enumerator.Private("_relationalCommandCache");

            var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
            var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");

            var sqlGenerator = factory.Create();
            var command = sqlGenerator.GetCommand(selectExpression);

            var queryContext = enumerator.Private<RelationalQueryContext>("_relationalQueryContext");

            var parametersDict = queryContext.ParameterValues;

            return (command, parametersDict);
        }

        //public static DbContext GetDbContext(this IQueryable query)
        //{
        //    var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
        //    var queryCompiler = typeof(EntityQueryProvider).GetField("_queryCompiler", bindingFlags).GetValue(query.Provider);
        //    var queryContextFactory = queryCompiler.GetType().GetField("_queryContextFactory", bindingFlags).GetValue(queryCompiler);

        //    //var dependencies = typeof(RelationalQueryContextFactory).GetProperty("Dependencies", bindingFlags).GetValue(queryContextFactory);

        //    var dependencies = typeof(RelationalQueryContextFactory).GetField("_dependencies", bindingFlags).GetValue(queryContextFactory);

        //    var queryContextDependencies = typeof(DbContext).Assembly.GetType(typeof(QueryContextDependencies).FullName);
        //    var stateManagerProperty = queryContextDependencies.GetProperty("StateManager", bindingFlags | BindingFlags.Public).GetValue(dependencies);
        //    var stateManager = (IStateManager)stateManagerProperty;

        //    return stateManager.Context;
        //}

        public static void AddParameters(this IDbCommand cmd, DbContext ctx, IReadOnlyDictionary<string, object> parameters)
        {
            var typeMapping = ctx.GetService<IRelationalTypeMappingSource>();
            foreach (var p in parameters)
            {
                if (p.Value != null)
                {
                    var mappedType = typeMapping.FindMapping(p.Value.GetType());
                    //the parameter type is not supported by underlying database.
                    //the value may be EF.Functions.ContainsOrEqual, int[] that have been translated into SQL clause.
                    //like Where(m => EF.Functions.ContainsOrEqual(m.IPv4.Value, ip)), and Where(p=>ids.Contains(p.Id)),
                    //so it's ignored.
                    if (mappedType == null)
                    {
                        continue;
                    }
                }
                var dbParam = cmd.CreateParameter();
                dbParam.ParameterName = p.Key;
                //fix issue on SQLServer: https://github.com/yangzhongke/Zack.EFCore.Batch/issues/26 
                if (p.Value == null)
                {
                    dbParam.Value = DBNull.Value;
                }
                else
                {
                    dbParam.Value = p.Value;
                }
                cmd.Parameters.Add(dbParam);
            }
        }

        private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
        private static T Private<T>(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
    }
}